查看原文
其他

局部变量竟然比全局变量快 5 倍?

磊哥 Java中文社群 2021-06-30

这是我的第 201 期分享

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

喽,大家好,磊哥的性能优化篇又来了!

其实写这个性能优化类的文章初衷也很简单,第一:目前市面上没有太好的关于性能优化的系列文章,包括一些付费的文章;第二:我需要写一些和别人不同的知识点,比如大家都去写 SpringBoot 了,那我就不会把重点全部放在 SpringBoot 上。而性能优化方面的文章又比较少,因此这就是我写它的理由。

至于能不能用上?是不是刚需?我想每个人都有自己的答案。就像一个好的剑客,终其一生都会对宝剑痴迷,我相信读到此文的你也是一样。

回到今天的主题,这次我们来评测一下局部变量和全局变量的性能差异,首先我们先在项目中先添加 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)测试框架,配置如下:

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>{version}</version>
</dependency>

然后编写测试代码:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1// fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class VarOptimizeTest {

    char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " +
            "automated migration Oracle Cloud Infrastructure platform is built for " +
            "enterprises that are looking for higher performance computing with easy " +
            "migration of their on-premises applications to the Cloud.").toCharArray();

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int globalVarTest() {
        int count = 0;
        for (int i = 0; i < myChars.length; i++) {
            if (myChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int localityVarTest() {
        char[] localityChars = myChars;
        int count = 0;
        for (int i = 0; i < localityChars.length; i++) {
            if (localityChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }
}

其中 globalVarTest 方法使用的是全局变量 myChars 进行循环遍历的,而 localityVarTest 方法使用的是局部变量 localityChars 来进行遍历循环的,使用 JMH 测试的结果如下:

咦,什么鬼?这两个方法的性能不是差不多嘛!为毛,你说差 5 倍?


CPU Cache

上面的代码之所以性能差不多其实是因为,全局变量 myChars 被 CPU 缓存了,每次我们查询时不会直接从对象的实例域(对象的实际存储结构)中查询的,而是直接从 CPU 的缓存中查询的,因此才有上面的结果。

为了还原真实的性能(局部变量和全局变量),因此我们需要使用 volatile 关键来修饰 myChars 全局变量,这样 CPU 就不会缓存此变量了, volatile 原本的语义是禁用 CPU 缓存的,我们修改的代码如下:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1// fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class VarOptimizeTest {

    volatile char[] myChars = ("Oracle Cloud Infrastructure Low data networking fees and " +
            "automated migration Oracle Cloud Infrastructure platform is built for " +
            "enterprises that are looking for higher performance computing with easy " +
            "migration of their on-premises applications to the Cloud.").toCharArray();

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(VarOptimizeTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int globalVarTest() {
        int count = 0;
        for (int i = 0; i < myChars.length; i++) {
            if (myChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int localityVarTest() {
        char[] localityChars = myChars;
        int count = 0;
        for (int i = 0; i < localityChars.length; i++) {
            if (localityChars[i] == 'c') {
                count++;
            }
        }
        return count;
    }
}

最终的测试结果是:


从上面的结果可以看出,局部变量的性能比全局变量的性能快了大约 5.02 倍

至于为什么局部变量会比全局变量快?咱们稍后再说,我们先来聊聊 CPU 缓存的事。

在计算机系统中,CPU 缓存(CPU Cache)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于 CPU 寄存器,如下图所示:


CPU 缓存的容量远小于内存,但速度却可以接近处理器的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

CPU 缓存可以分为一级缓存(L1),二级缓存(L2),部分高端 CPU 还具有三级缓存(L3),这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

以下是各级缓存和内存响应时间的对比图:

(图片来源:cenalulu)

从上图可以看出内存的响应速度要比 CPU 缓存慢很多


局部变量为什么快?

要理解为什么局部变量会比全局变量快这个问题,我们只需要使用 javac 把他们编译成字节码就可以找到原因了,编译的字节码如下:

javap -c VarOptimize
警告: 文件 ./VarOptimize.class 不包含类 VarOptimize
Compiled from "VarOptimize.java"
public class com.example.optimize.VarOptimize {
  char[] myChars;

  public com.example.optimize.VarOptimize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #7                  // String Oracle Cloud Infrastructure Low data networking fees and automated migration Oracle Cloud Infrastructure platform is built for enterprises that are looking for higher performance computing with easy migration of their on-premises applications to the Cloud.
       7: invokevirtual #9                  // Method java/lang/String.toCharArray:()[C
      10: putfield      #15                 // Field myChars:[C
      13return

  public static void main(java.lang.String[]);
    Code:
       0new           #16                 // class com/example/optimize/VarOptimize
       3: dup
       4: invokespecial #21                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #22                 // Method globalVarTest:()V
      12: aload_1
      13: invokevirtual #25                 // Method localityVarTest:()V
      16return

  public void globalVarTest();
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: aload_0
       6: getfield      #15                 // Field myChars:[C
       9: arraylength
      10: if_icmpge     33
      13: aload_0
      14: getfield      #15                 // Field myChars:[C
      17: iload_2
      18: caload
      19: bipush        99
      21: if_icmpne     27
      24: iinc          11
      27: iinc          21
      30goto          4
      33return

  public void localityVarTest();
    Code:
       0: aload_0
       1: getfield      #15                 // Field myChars:[C
       4: astore_1
       5: iconst_0
       6: istore_2
       7: iconst_0
       8: istore_3
       9: iload_3
      10: aload_1
      11: arraylength
      12: if_icmpge     32
      15: aload_1
      16: iload_3
      17: caload
      18: bipush        99
      20: if_icmpne     26
      23: iinc          21
      26: iinc          31
      29goto          9
      32return
}

其中关键的信息就在 getfield 关键字上,getfield 在此处的语义是从堆上获取变量,从上述的字节码可以看出 globalVarTest 方法在循环的内部每次都通过 getfield 关键字从堆上获取变量,而 localityVarTest 方法并没有使用 getfield 关键字,而是使用了出栈操作来进行业务处理,而从堆中获取变量比出栈操作要慢很多,因此使用全局变量会比局部变量慢很多。关于堆、栈的内容关注公众号「Java中文社群」我在后面的 JVM 优化的章节会单独讲解。

关于缓存

有人可能会说无所谓,反正使用全局变量会使用 CPU Cache,这样性能也和局部变量差不多,那我就随便用吧,反正也差不多。

但磊哥的建议是,能用局部变量的绝不使用全局变量,因为 CPU 缓存有以下 3 个问题:

  1. CPU Cache 采用的是 LRU 和 Random 的清除算法,不常使用的缓存和随机抽取一部分缓存会被删除掉,如果正好是你用的那个全局变量呢?
  2. CPU Cache 有缓存命中率的问题,也就是有一定的几率会访问不到缓存;
  3. 部分 CPU 只有两级缓存(L1 和 L2),因此可以使用的空间是有限的。

综上所述,我们不能把程序的执行性能完全托付给一个不那么稳定的系统硬件,所以能用局部变量坚决不要使用全局变量

关键点:编写适合你的代码,在性能、可读性和实用性之间,找到属于你的平衡点!

总结

本文我们讲了局部变量的和全局变量的区别,如果使用全局变量会用 getfield 关键字从堆中获取变量,而局部变量则是通过出栈来获取变量的,因为出栈操作要比堆操作快很多,因此局部变量操作也会比全局变量快很多,所以建议你使用局部变量而不是全局变量。

高手之间对决,比拼的就是细节。

最后的话

原创不易,觉得有帮助,点个「在看」让我知道,谢谢你!



往期推荐

if快还是switch快?解密switch背后的秘密


本来想用“{{”秀一波,结果却导致了内存溢出!


觉得本文有帮助,点击“在看”鼓励下我吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存